package com.zulip.android.util;
import android.text.Editable;
import android.text.Html;
import android.text.Spanned;
import android.text.style.BulletSpan;
import android.text.style.LeadingMarginSpan;
import android.util.Log;
import org.xml.sax.XMLReader;
import java.util.Stack;
/**
* Custom TagHandler {@link Html.TagHandler} to support ordered and unordered list in
* TextView {@link android.widget.TextView}.
*/
public class ListTagHandler implements Html.TagHandler {
private static final String LOG_TAG = ListTagHandler.class.getSimpleName();
private static final String OL_TAG = "ol";
private static final String UL_TAG = "ul";
private static final String LI_TAG = "li";
/**
* List indentation in pixels.
*/
private static final int INDENT_PX = 10;
private static final int LIST_ITEM_INDENT_PX = INDENT_PX * 2;
private static final BulletSpan BULLET_SPAN = new BulletSpan(INDENT_PX);
/**
* Keeps track of lists (ol, ul). On bottom of Stack is the outermost list
* and on top of Stack is the most nested list.
*/
private final Stack<ListTag> lists = new Stack<ListTag>();
@Override
public void handleTag(final boolean opening, final String tag, final Editable output, final XMLReader xmlReader) {
if (UL_TAG.equalsIgnoreCase(tag)) {
if (opening) {
// handle <ul>
lists.push(new Ul());
} else {
// handle </ul>
lists.pop();
}
} else if (OL_TAG.equalsIgnoreCase(tag)) {
if (opening) {
// handle <ol>
// use default start index of 1
lists.push(new Ol());
} else {
// handle </ol>
lists.pop();
}
} else if (LI_TAG.equalsIgnoreCase(tag)) {
if (opening) {
// handle <li>
lists.peek().openItem(output);
} else {
// handle </li>
lists.peek().closeItem(output, lists.size());
}
} else {
Log.d(LOG_TAG, "Found an unsupported tag " + tag);
}
}
/**
* Abstract super class for {@link Ul} and {@link Ol}.
*/
private abstract static class ListTag {
/**
* Opens a new list item.
*
* @param text
*/
public void openItem(final Editable text) {
if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
text.append("\n");
}
final int len = text.length();
text.setSpan(this, len, len, Spanned.SPAN_MARK_MARK);
}
/**
* Closes a list item.
*
* @param text
* @param indentation
*/
public final void closeItem(final Editable text, final int indentation) {
if (text.length() > 0 && text.charAt(text.length() - 1) != '\n') {
text.append("\n");
}
final Object[] replaces = getReplaces(text, indentation);
final int len = text.length();
final ListTag listTag = getLast(text);
final int where = text.getSpanStart(listTag);
text.removeSpan(listTag);
if (where != len) {
for (Object replace : replaces) {
text.setSpan(replace, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
protected abstract Object[] getReplaces(final Editable text, final int indentation);
/**
* Note: This knows that the last returned object from getSpans() will be the most recently added.
*/
private ListTag getLast(final Spanned text) {
final ListTag[] listTags = text.getSpans(0, text.length(), ListTag.class);
if (listTags.length == 0) {
return null;
}
return listTags[listTags.length - 1];
}
}
/**
* Class representing the unordered list <ul></ul> HTML tag.
*/
private static class Ul extends ListTag {
@Override
protected Object[] getReplaces(final Editable text, final int indentation) {
// Nested BulletSpans increases distance between BULLET_SPAN and text, so we must prevent it.
int bulletMargin = INDENT_PX;
if (indentation > 1) {
bulletMargin = INDENT_PX - BULLET_SPAN.getLeadingMargin(true);
if (indentation > 2) {
// This get's more complicated when we add a LeadingMarginSpan into the same line:
// we have also counter it's effect to BulletSpan
bulletMargin -= (indentation - 2) * LIST_ITEM_INDENT_PX;
}
}
return new Object[]{
new LeadingMarginSpan.Standard(LIST_ITEM_INDENT_PX * (indentation - 1)),
new BulletSpan(bulletMargin)
};
}
}
/**
* Class representing the ordered list <ol></ol> HTML tag.
*/
private static class Ol extends ListTag {
private int nextIdx;
/**
* Creates a new <ol></ol> with start index of 1.
*/
public Ol() {
this(1); // default start index
}
/**
* Creates a new <ol></ol> with given start index.
*
* @param startIdx
*/
public Ol(final int startIdx) {
this.nextIdx = startIdx;
}
@Override
public void openItem(final Editable text) {
super.openItem(text);
text.append(Integer.toString(nextIdx++)).append(". ");
}
@Override
protected Object[] getReplaces(final Editable text, final int indentation) {
int numberMargin = LIST_ITEM_INDENT_PX * (indentation - 1);
if (indentation > 2) {
// Same as in ordered lists: counter the effect of nested Spans
numberMargin -= (indentation - 2) * LIST_ITEM_INDENT_PX;
}
return new Object[]{new LeadingMarginSpan.Standard(numberMargin)};
}
}
}